const express = require('express')
var expressWs = require('express-ws')
var cookieParser = require('cookie-parser')
var session = require('express-session')

const next = require('next')
const contentDisposition = require('content-disposition')
const mime = require('mime-types')
const fs = require('fs')
const { stat, createReadStream, readdir, accessSync } = fs
const crypto = require('crypto')
const path = require('path')
const bytes = require('bytes')
var db = require('./db')

const { promisify } = require('./utils')
const parseRange = require('range-parser')
const { getClients } = require('./mqtt/mqtt_server')
const { registerFileWatch, unregisterFileWatcher } = require('./utils/logutils')
var api = require('./routes/api')
var LevelStore = require('level-session-store')(session)
const dev = process.env.NODE_ENV !== 'production'
const app = next({
  dev
})
const handle = app.getRequestHandler()

const handlers = {
  stat: promisify(stat),
  createReadStream,
  readdir: promisify(readdir),
  access: accessSync
}

Date.prototype.Format = function(fmt) {
  //author: meizz
  var o = {
    'M+': this.getMonth() + 1, //月份
    'd+': this.getDate(), //日
    'h+': this.getHours(), //小时
    'm+': this.getMinutes(), //分
    's+': this.getSeconds(), //秒
    'q+': Math.floor((this.getMonth() + 3) / 3), //季度
    S: this.getMilliseconds() //毫秒
  }
  if (/(y+)/.test(fmt))
    fmt = fmt.replace(
      RegExp.$1,
      (this.getFullYear() + '').substr(4 - RegExp.$1.length)
    )
  for (var k in o)
    if (new RegExp('(' + k + ')').test(fmt))
      fmt = fmt.replace(
        RegExp.$1,
        RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
      )
  return fmt
}

const sortfile = files => {
  return new Promise((reslove, reject) => {
    const ret = files.sort((a, b) => {
      const aIsDir = a.type === 'directory'
      const bIsDir = b.type === 'directory'

      /* istanbul ignore next */
      if (aIsDir && !bIsDir) {
        return -1
      }

      if (bIsDir && !aIsDir) {
        return 1
      }

      if (a.createtime > b.createtime) {
        return -1
      } else {
        return 1
      }

      /* istanbul ignore next */
      return 0
    })
    reslove(ret)
  })
}

const getHeaders = async (customHeaders = [], current, absolutePath, stats) => {
  const related = {}
  const { base } = path.parse(absolutePath)
  const relativePath = path.relative(current, absolutePath)

  if (customHeaders.length > 0) {
    // By iterating over all headers and never stopping, developers
    // can specify multiple header sources in the config that
    // might match a single path.
    for (let index = 0; index < customHeaders.length; index++) {
      const { source, headers } = customHeaders[index]

      if (sourceMatches(source, slasher(relativePath))) {
        appendHeaders(related, headers)
      }
    }
  }

  let defaultHeaders = {}

  if (stats) {
    defaultHeaders = {
      'Last-Modified': stats.mtime.toUTCString(),
      'Content-Length': stats.size,
      // Default to "inline", which always tries to render in the browser,
      // if that's not working, it will save the file. But to be clear: This
      // only happens if it cannot find a appropiate value.
      'Content-Disposition': contentDisposition(base, {
        type: 'inline'
      }),
      'Accept-Ranges': 'bytes'
    }

    const contentType = mime.contentType(base)

    if (contentType) {
      defaultHeaders['Content-Type'] = contentType
    }
  }

  const headers = Object.assign(defaultHeaders, related)

  for (const key in headers) {
    if (headers.hasOwnProperty(key) && headers[key] === null) {
      delete headers[key]
    }
  }

  return headers
}

const stateDirFiles = async (absolutePath, parent) => {
  let files = await handlers.readdir(path.resolve(absolutePath, parent))
  for (let index = 0; index < files.length; index++) {
    const file = files[index]
    const filePath = path.resolve(path.resolve(absolutePath, parent), file)
    const details = path.parse(filePath)
    let stats = await handlers.stat(path.resolve(absolutePath, filePath))
    details.relative = '/' + path.join(parent, details.base)
    details.createtime = stats.mtimeMs
    details.mtime = new Date(stats.mtimeMs).Format('yyyy-MM-dd hh:mm:ss')
    if (stats.isDirectory()) {
      details.type = 'directory'
      details.name = details.base
      details.ext = ''
    } else {
      details.ext = details.ext.split('.')[1] || 'txt'
      details.type = 'file'

      details.size = bytes(stats.size, {
        unitSeparator: ' ',
        decimalPlaces: 0
      })
    }
    files[index] = details
  }
  files = await sortfile(files)
  return files
}

app
  .prepare()
  .then(() => {
    const server = express()
    expressWs(server)
    server.use(express.json())
    server.use(
      express.urlencoded({
        extended: false
      })
    )
    server.use(cookieParser())
    server.use(
      session({
        secret: '12345',
        resave: true,
        store: new LevelStore('../session'),
        saveUninitialized: true,
        cookie: {
          maxAge: 12 * 3600 * 1000
        } //设置maxAge是12h，即12h后session和相应的cookie失效过期
      })
    )
    const ignorePath = ['/user/login', '/q']
    server.use('/api', (req, res, next) => {
      if (ignorePath.indexOf(req.url) >= 0) {
        next()
      } else {
        if (!req.session.user) {
          res.statusCode = 401
          res.cookie('authority', 'guest')
          res.send({
            msg: '没有登录'
          })
        } else {
          next()
        }
      }
    })
    server.use('/api', api)
    server.ws('/ws/*', (ws, req) => {
      const clients = getClients()
      let relativePath = decodeURIComponent(req.url)
      let clientId = relativePath.substr(
        '/ws/'.length,
        relativePath.lastIndexOf('/') - '/ws/'.length
      )
      if (clients[clientId]) {
        let file = `${clients[clientId].udid}_${clients[clientId].package}`
        let watcherCb = line => {
          ws.send(line)
        }
        registerFileWatch(file, watcherCb)
        ws.on('close', () => {
          unregisterFileWatcher(file, watcherCb)
        })
      } else {
        ws.close()
      }
    })
    server.get('/links*', (req, res) => {
      const clients = getClients()
      let relativePath = decodeURIComponent(req.url)
      let file = relativePath.substr('/links'.length)
      if (file.length > 0) {
        if (file.indexOf('/') === 0) {
          if (clients[file.substr(1)]) {
            const deviceInfo = []
            for (var key in clients[file.substr(1)]) {
              deviceInfo.push({
                key,
                val: clients[file.substr(1)][key]
              })
            }
            app.render(req, res, '/deviceInfo', {
              deviceInfo
            })
          } else {
            app.render404(req, res)
          }
        } else {
          app.render404(req, res)
        }
      } else {
        const files = []
        for (var key in clients) {
          files.push({
            name: clients[key].device,
            relative: '/links/' + key,
            type: 'file',
            count: 0,
            ext: 'apk',
            md5: clients[key].package
          })
        }
        app.render(req, res, '/', {
          title: files
        })
      }
    })

    server.get('/*', async (req, res) => {
      let relativePath = decodeURIComponent(req.url)
      let file = relativePath.substr(1)
      let absolutePath = process.env.absolutePath
      try {
        handlers.access(path.resolve(absolutePath, file))
      } catch (err) {
        return handle(req, res)
      }
      let stats = await handlers.stat(path.resolve(absolutePath, file))
      if (stats && stats.isDirectory()) {
        let files = await load(file)
        if (file === '') {
          files.push({
            ext: '',
            name: 'links',
            relative: '/links',
            type: 'directory'
          })
        }
        const queryParams = {
          title: files
        }

        app.render(req, res, '/', queryParams)
        return
      }
      if (!stats) {
        return handle(req, res)
      }
      const streamOpts = {}

      // TODO ? if-range
      if (req.headers.range && stats.size) {
        const range = parseRange(stats.size, req.headers.range)

        if (typeof range === 'object' && range.type === 'bytes') {
          const { start, end } = range[0]
          streamOpts.start = start
          streamOpts.end = end

          res.statusCode = 206
        } else {
          res.statusCode = 416
          res.setHeader('Content-Range', `bytes */${stats.size}`)
        }
      } else {
        update(
          file.substr(0, file.lastIndexOf('/')),
          `${file.substr(
            file.lastIndexOf('/') + 1,
            file.lastIndexOf('.') - file.lastIndexOf('/') - 1
          )}_${new Date(stats.mtimeMs).Format('yyyy-MM-dd hh:mm:ss')}`
        )
      }
      let stream = null

      try {
        stream = await handlers.createReadStream(
          path.resolve(absolutePath, file),
          streamOpts
        )
      } catch (err) {
        return handle(req, res)
      }

      const headers = await getHeaders(
        {},
        '',
        path.resolve(absolutePath, file),
        stats
      )

      // eslint-disable-next-line no-undefined
      if (streamOpts.start !== undefined && streamOpts.end !== undefined) {
        headers['Content-Range'] = `bytes ${streamOpts.start}-${
          streamOpts.end
        }/${stats.size}`
        headers['Content-Length'] = streamOpts.end - streamOpts.start + 1
      }
      if (
        req.headers.range == null &&
        headers.ETag &&
        headers.ETag === req.headers['if-none-match']
      ) {
        res.statusCode = 304
        res.end()
        return
      }
      res.writeHead(res.statusCode || 200, headers)
      stream.pipe(res)
    })

    server.listen(process.env.PORT, err => {
      if (err) throw err
      console.log('> Ready on http://localhost:3000')
    })
  })
  .catch(ex => {
    console.error(ex.stack)
    process.exit(1)
  })

const update = async (dirName, fileState) => {
  let absolutePath = process.env.absolutePath
  let savedState = {}
  try {
    savedState = await db.get(dirName)
    savedState = JSON.parse(savedState)
  } catch (e) {}
  let files = await stateDirFiles(absolutePath, dirName)
  const _storeState = {}
  for (var i = 0; i < files.length; i++) {
    let d = files[i]
    const key = `${d.name}_${d.mtime}`
    const value = {}
    if (typeof savedState[key] === 'number') {
      value.count = savedState[key] || 0
    } else {
      value.count = savedState[key] ? savedState[key].count || 0 : 0
    }
    if (d.type === 'file') {
      if (typeof savedState[key] === 'object') {
        value.md5 =
          savedState[key].md5 || (await readFileMd5(path.join(d.dir, d.base)))
      } else {
        value.md5 = await readFileMd5(path.join(d.dir, d.base))
      }
    }
    _storeState[key] = value
  }
  _storeState[fileState].count = _storeState[fileState].count + 1
  try {
    await db.put(dirName || '/', JSON.stringify(_storeState))
  } catch (e) {
    console.log(e)
  }
}

const load = async dirName => {
  let absolutePath = process.env.absolutePath
  let savedState = {}
  try {
    savedState = await db.get(dirName)
    savedState = JSON.parse(savedState)
  } catch (e) {}
  let files = await stateDirFiles(absolutePath, dirName)
  for (var i = 0; i < files.length; i++) {
    let d = files[i]
    const key = `${d.name}_${d.mtime}`
    if (typeof savedState[key] === 'number') {
      d.count = savedState[key] || 0
    } else {
      d.count = savedState[key] ? savedState[key].count || 0 : 0
    }
    if (d.type === 'file') {
      if (typeof savedState[key] === 'object') {
        d.md5 =
          savedState[key].md5 || (await readFileMd5(path.join(d.dir, d.base)))
      } else {
        d.md5 = await readFileMd5(path.join(d.dir, d.base))
      }
    }
    savedState[key] = d
  }
  try {
    await db.put(dirName || '/', JSON.stringify(savedState))
  } catch (e) {
    console.log(e)
  }
  return files
}

const readFileMd5 = url => {
  // console.log(url)
  return new Promise(reslove => {
    let md5sum = crypto.createHash('md5')
    let stream = fs.createReadStream(url)
    stream.on('data', function(chunk) {
      md5sum.update(chunk)
    })
    stream.on('end', function() {
      let fileMd5 = md5sum.digest('hex')
      reslove(fileMd5)
    })
  })
}
